package de.rj.urwalking.ui.fragments;

import java.util.ArrayList;

import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;

import com.threed.jpct.SimpleVector;

import de.ralf.urwalking.soplugin.EPSGPoint;
import de.rj.urwalking.R;
import de.rj.urwalking.config.Config;
import de.rj.urwalking.config.Constants;
import de.rj.urwalking.data.Building;
import de.rj.urwalking.data.Perspective;
import de.rj.urwalking.data.Pin;
import de.rj.urwalking.data.Poi;
import de.rj.urwalking.data.SalientObject;
import de.rj.urwalking.db.DBSalientObject;
import de.rj.urwalking.loader.GeometryLoader;
import de.rj.urwalking.loader.PoiLoader;
import de.rj.urwalking.loader.SalientObjectLoader;
import de.rj.urwalking.ui.OutdoorRenderer;
import de.rj.urwalking.ui.activities.OutdoorActivity;
import de.rj.urwalking.ui.activities.URwalkingActivity;
import de.rj.urwalking.ui.fragments.dialog.BuildingDialogFragment;
import de.rj.urwalking.ui.fragments.dialog.LandmarkDialogFragment;
import de.rj.urwalking.ui.fragments.dialog.NewLandmarkDialogFragment;
import de.rj.urwalking.ui.fragments.dialog.PinDialogFragment;
import de.rj.urwalking.ui.fragments.dialog.PoiDialogFragment;
import de.rj.urwalking.util.ExperimentManager;
import de.rj.urwalking.util.GeoUtil;
import de.rj.urwalking.util.LocationUtil;
import de.rj.urwalking.util.PrefUtil;
import de.rj.urwalking.util.Util;
import de.rose_mobil.models.route.Route;

/**
 * 
 * @author rjj
 * 
 */
public class OutdoorFragment extends JpctFragment {

	private OutdoorRenderer renderer;

	/** the SO that is selected at the moment, indicated by a color highlight */
	private SalientObject currentlySelectedObject;
	private SimpleVector offset;
	private Location mLocation;

	private LoaderCallbacks<ArrayList<Building>> geometryLoaderCallbacks = new GeometryLoaderCallbacks();
	private LoaderCallbacks<ArrayList<Poi>> poiLoaderCallbacks = new PoiLoaderCallbacks();
	private LoaderCallbacks<ArrayList<DBSalientObject>> salientObjectLoaderCallbacks = new SalientObjectLoaderCallbacks();
	private RelativeLayout arrows;
	private SimpleVector objectPosition;
	protected SimpleVector lastManualLoadLocation = new SimpleVector(0, 0, 0);
	private float mOrientation;
	private Location mLastLoadedLocation;

	private int mPerspective;

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		parseArgs();
		View rootView = inflater.inflate(R.layout.fragment_outdoor, container, false);
		initUI(rootView);
		init3D(rootView);

		tryLoading();

		return rootView;
	}

	@Override
	public void onSaveInstanceState(Bundle outState) {
		super.onSaveInstanceState(outState);

		outState.putSerializable("offset", offset);

	}

	@Override
	public void onActivityCreated(Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);
		if (savedInstanceState != null) {
			offset = (SimpleVector) savedInstanceState.getSerializable("offset");
		}

	}

	private void parseArgs() {

		if (getArguments() == null) {
			if (Config.DEBUG) Log.w(Config.TAG, "no arguments supplied for OutdoorFragment!");
			return;
		}

		mPerspective = getArguments().getInt("perspective");
		if (mPerspective == 0) {
			mPerspective = Perspective.TYPE_3D_ABOVE;
			Log.w(Config.TAG, "No Perspective specified for OutdoorFragment! Using default view from above.");
		}

		double cameraLat = getArguments().getDouble("cameraLat");
		double cameraLon = getArguments().getDouble("cameraLon");

		double objectLat = getArguments().getDouble("objectLat");
		double objectLon = getArguments().getDouble("objectLon");

		if (cameraLat != 0.0 && cameraLon != 0.0) {
			// use the camera position as initial position and offset
			mLocation = new Location(LocationManager.GPS_PROVIDER);
			mLocation.setLatitude(cameraLat);
			mLocation.setLongitude(cameraLon);

			EPSGPoint epsgcoords = GeoUtil.convertLatLonToEPSG31468(cameraLon, cameraLat);
			offset = new SimpleVector((int) epsgcoords.getX(), (int) epsgcoords.getY(), 0);

			if (objectLat != 0.0 && objectLon != 0.0) {
				epsgcoords = GeoUtil.convertLatLonToEPSG31468(objectLon, objectLat);
				objectPosition = new SimpleVector((int) epsgcoords.getX() - offset.x, (int) epsgcoords.getY()
						- offset.y, 0);

			}
		}

	}

	private void init3D(View rootView) {

		// show arrows only in street view
		arrows = (RelativeLayout) rootView.findViewById(R.id.arrows);
		if (mPerspective == Perspective.TYPE_3D_ABOVE) {
			arrows.setVisibility(View.GONE);
		}

		renderer = new OutdoorRenderer(getActivity(), this, mPerspective);
		super.renderer = renderer;
		mGLView.setRenderer(renderer);

	}

	private void showLandmarkDialog(SalientObject object) {

		LandmarkDialogFragment landmarkDialogFragment = null;
		if (object instanceof Building) {
			landmarkDialogFragment = new BuildingDialogFragment();
		} else if (object instanceof Poi) {
			landmarkDialogFragment = new PoiDialogFragment();
		} else if (object instanceof Pin && ((Pin) object).isExistingLandmark()) {
			((Pin) object).setOffset(offset);
			landmarkDialogFragment = new PinDialogFragment();
		} else {
			((Pin) object).setOffset(offset);
			landmarkDialogFragment = new NewLandmarkDialogFragment();
		}

		Bundle args = new Bundle();
		args.putSerializable("object", object);
		args.putSerializable("projectionMatrix", renderer.getProjectionMatrix());
		args.putDouble("currentLatitude", mLocation.getLatitude());
		args.putDouble("currentLongitude", mLocation.getLongitude());

		landmarkDialogFragment.setArguments(args);
		landmarkDialogFragment.show(getChildFragmentManager(), "fragment_landmark_dialog");
	}

	private void tryLoading() {
		if (offset != null) {
			manuallyLoadData();
		}
	}

	public void manuallyLoadData() {
		if (Util.hasConnection(getActivity()) && !getLoaderManager().hasRunningLoaders()) {
			restartLoaders(true);
		}
	}

	public void restartLoaders(boolean manualLoad) {

		Bundle args = new Bundle();
		args.putBoolean("manual_load", manualLoad);

		LoaderManager lm = getActivity().getSupportLoaderManager();
		lm.restartLoader(Constants.GEOMETRY_LOADER_ID, args, geometryLoaderCallbacks);
		lm.restartLoader(Constants.POI_LOADER_ID, args, poiLoaderCallbacks);

		if (PrefUtil.readBoolean(R.string.key_pref_show_landmarks, getActivity(), true)) {
			lm.restartLoader(Constants.SALIENT_OBJECT_LOADER_ID, args, salientObjectLoaderCallbacks);
		}

	}

	private class GeometryLoaderCallbacks implements LoaderCallbacks<ArrayList<Building>> {

		public Loader<ArrayList<Building>> onCreateLoader(int id, Bundle args) {
			showLoadingIndicator();
			GeometryLoader loader = null;
			if (args.getBoolean("manual_load")) {
				loader = new GeometryLoader(getActivity(), offset, lastManualLoadLocation);
			} else {
				loader = new GeometryLoader(getActivity(), offset, mLocation);
			}
			return loader;
		}

		public void onLoadFinished(Loader<ArrayList<Building>> loader, ArrayList<Building> data) {
			Log.d("lego", "outdoor");
			renderer.addBuildings(data);
			renderer.setBuildingsVisibility(PrefUtil.readBoolean(R.string.key_pref_show_buildings, getActivity(), true));
			renderer.setBuildingsOpacity(PrefUtil
					.readBoolean(R.string.key_pref_buildings_opacity, getActivity(), false));
			renderer.checkSalientObjects();
			renderer.removeFarObjects();

			hideLoadingIndicator();

		}

		public void onLoaderReset(Loader<ArrayList<Building>> loader) {
		}

	}

	private class PoiLoaderCallbacks implements LoaderCallbacks<ArrayList<Poi>> {

		public Loader<ArrayList<Poi>> onCreateLoader(int id, Bundle args) {
			showLoadingIndicator();
			PoiLoader loader = null;
			if (args.getBoolean("manual_load")) {
				loader = new PoiLoader(getActivity(), offset, lastManualLoadLocation);
			} else {
				loader = new PoiLoader(getActivity(), offset, mLocation);
			}
			return loader;
		}

		public void onLoadFinished(Loader<ArrayList<Poi>> loader, ArrayList<Poi> data) {

			renderer.addPois(data);
			renderer.setPoiVisibility(PrefUtil.readPoiVisibility(getActivity()));
		}

		public void onLoaderReset(Loader<ArrayList<Poi>> loader) {
		}

	}

	private class SalientObjectLoaderCallbacks implements LoaderCallbacks<ArrayList<DBSalientObject>> {

		public Loader<ArrayList<DBSalientObject>> onCreateLoader(int id, Bundle args) {

			SalientObjectLoader loader = null;
			if (args.getBoolean("manual_load")) {
				loader = new SalientObjectLoader(getActivity(), lastManualLoadLocation.calcAdd(offset), mOrientation);
			} else {
				loader = new SalientObjectLoader(getActivity(), mLocation, mOrientation);
			}
			return loader;

		}

		public void onLoadFinished(Loader<ArrayList<DBSalientObject>> loader, ArrayList<DBSalientObject> data) {

			for (DBSalientObject dbSalientObject : data) {
				dbSalientObject.applyOffset(offset);
			}

			renderer.addSalientObjects(data);

			if (getActivity() instanceof OutdoorActivity) {
				((URwalkingActivity) getActivity()).updateSideBar(data);
			}
		}

		public void onLoaderReset(Loader<ArrayList<DBSalientObject>> loader) {
		}

	}

	@Override
	public boolean onTouch(View v, MotionEvent event) {
		if (PrefUtil.readBoolean(R.string.key_pref_auto_load, getActivity(), false)
				&& LocationUtil.hasMovedSignificantly(renderer.getCameraPosition(), lastManualLoadLocation)) {
			lastManualLoadLocation = new SimpleVector(renderer.getCameraPosition().x, renderer.getCameraPosition().y, 0);
			manuallyLoadData();
		}

		switch (v.getId()) {
		case R.id.buttonLeft:
			return handleButtonLeft(event);
		case R.id.buttonRight:
			return handleButtonRight(event);
		case R.id.buttonForward:
			return handleButtonForward(event);
		case R.id.buttonBackward:
			return handleButtonBackward(event);
		case R.id.surfaceView:
			return handleSurfaceTouch(event);
		}

		return false;
	}

	private boolean handleSurfaceTouch(MotionEvent event) {
		// detects gestures like short/long presses or double taps
		boolean gestureDetected = gestureDetector.onTouchEvent(event);
		// detects pinch zooming
		scaleGestureDetector.onTouchEvent(event);

		if (!scaleGestureDetector.isInProgress()) {

			if (mPerspective == Perspective.TYPE_3D_ABOVE) {
				boolean hasMoved = renderer.adjustCameraOverview(event);
				if (hasMoved && mActivity.getFollowGPS()) {
					mActivity.disableGpsFollowing(false);
				}

				if (PrefUtil.readBoolean(R.string.key_pref_auto_load, getActivity(), false)
						&& LocationUtil.hasMovedSignificantly(renderer.getCameraPosition(), lastManualLoadLocation)) {
					lastManualLoadLocation = new SimpleVector(renderer.getCameraPosition().x,
							renderer.getCameraPosition().y, 0);
					manuallyLoadData();
				}

			} else if (mPerspective == Perspective.TYPE_3D_STREET) {
				boolean hasTurned = renderer.adjustEgoPerspective(event);
				if (hasTurned && mActivity.getUseCompass()) {
					mActivity.disableCompassMovement(true);
				} else if (!mActivity.getUseCompass()) {
					float direction = renderer.getCameraDirectionInRadians();
					mCompassView.updateDirection(-direction);
				}

			}
		}
		return gestureDetected;
	}

	public void changePerspective() {
		if (mPerspective == Perspective.TYPE_3D_ABOVE) {
			mPerspective = Perspective.TYPE_3D_STREET;
			if (ExperimentManager.getInstance().isExperimentRunning()) {
				ExperimentManager.getInstance().getCurrentExperiment().addViewChange(mPerspective);
			}
			arrows.setVisibility(View.VISIBLE);
			renderer.switchToStreetView();

		} else if (mPerspective == Perspective.TYPE_3D_STREET) {
			mPerspective = Perspective.TYPE_3D_ABOVE;
			if (ExperimentManager.getInstance().isExperimentRunning()) {
				ExperimentManager.getInstance().getCurrentExperiment().addViewChange(mPerspective);
			}
			arrows.setVisibility(View.GONE);
			renderer.switchToOverview();
		}

	}

	/**
	 * Checks all Preferences that may have changed
	 */
	public void checkPreferences() {

		if (PrefUtil.readBoolean(R.string.key_pref_show_landmarks, getActivity(), true)) {
			renderer.checkSalientObjects();
		} else {
			renderer.hideLandmarks();
		}

		renderer.setPoiVisibility(PrefUtil.readPoiVisibility(getActivity()));
		renderer.setBuildingsVisibility(PrefUtil.readBoolean(R.string.key_pref_show_buildings, getActivity(), true));
		renderer.setBuildingsOpacity(PrefUtil.readBoolean(R.string.key_pref_buildings_opacity, getActivity(), false));

	}

	/**
	 * Converts GPS location to the internally used EPSG31468 format. Checks if the new position is inside a building.
	 * If not, moves the user/camera to this position.
	 * 
	 * @param location
	 *            the new location (GPS coordinates)
	 */
	@Override
	public void setLocation(Location location) {

		EPSGPoint userPosition = GeoUtil.convertLatLonToEPSG31468(location.getLongitude(), location.getLatitude());

		// use this location as offset unless already set
		if (offset == null) {
			offset = new SimpleVector((int) userPosition.getX(), (int) userPosition.getY(), 0);
		}

		// load data if we got a location for the first time
		if (mLocation == null) {
			mLocation = location;
			restartLoaders(false);
		}

		if (LocationUtil.hasMovedSignificantly(location, mLastLoadedLocation) && Util.hasConnection(getActivity())
				&& !getActivity().getSupportLoaderManager().hasRunningLoaders()) {

			mLastLoadedLocation = location;
			restartLoaders(false);
			mActivity.updateLocationTextView();
		}

		// remember the location
		mLocation = location;

		// update 3d view, if ready
		if (renderer == null) return;
		if (!renderer.isInsideBuilding(userPosition.getX() - offset.x, userPosition.getY() - offset.y)) {
			renderer.moveUserTo((float) userPosition.getX() - offset.x, (float) userPosition.getY() - offset.y);
		}
	}

	@Override
	public void setOrientation(float orientation) {
		mOrientation = orientation;

		if (renderer != null) {
			renderer.setCameraAngle(orientation);
			renderer.updateUserArrow(orientation);
			mCompassView.updateDirection(orientation);

		}
	}

	public void newLandmark() {
		currentlySelectedObject = renderer.addLandmarkAtUserPosition();
		showLandmarkDialog(currentlySelectedObject);
	}

	// public int getCameraMode() {
	// return cameraMode;
	// }

	public void setObjectName(String name) {
		currentlySelectedObject.setUserDefinedName(name);
	}

	@Override
	public void setRoute(Route route) {
		// TODO Auto-generated method stub

	}

	@Override
	public void onScaleEnd(ScaleGestureDetector detector) {
	}

	@Override
	public boolean onScaleBegin(ScaleGestureDetector detector) {
		return true;
	}

	@Override
	public boolean onScale(ScaleGestureDetector detector) {
		float scaleFactor = detector.getScaleFactor();

		renderer.zoom(scaleFactor);

		return true;
	}

	@Override
	public boolean onDown(MotionEvent e) {
		return true;
	}

	/**
	 * Finds out if an object was tapped and displays a dialog.
	 */
	@Override
	public boolean onSingleTapConfirmed(MotionEvent e) {
		SalientObject pickedObject = renderer.checkRayCollision(e.getX(), e.getY());

		if (pickedObject == null) {
			return false;
		} else {
			currentlySelectedObject = pickedObject;
			showLandmarkDialog(pickedObject);
			return true;
		}
	}

	/**
	 * Same thing as above, for long presses.
	 */
	@Override
	public void onLongPress(MotionEvent e) {
		SalientObject pickedObject = renderer.checkRayCollision(e.getX(), e.getY());

		if (pickedObject == null) return;

		currentlySelectedObject = pickedObject;
		showLandmarkDialog(pickedObject);

	}

	@Override
	public boolean onDoubleTap(MotionEvent e) {

		if (mPerspective == Perspective.TYPE_3D_ABOVE) {
			renderer.zoomIn();
			return true;
		}
		return false;

	}

	public void setPerspective(int perspective) {

		// double check if we are already in this perspective (shouldn't actually be the case or we wouldn't even have
		// come here)
		if (perspective == mPerspective) return;

		switch (perspective) {
		case Perspective.TYPE_3D_ABOVE:
			renderer.switchToOverview();
			break;
		case Perspective.TYPE_3D_STREET:
			renderer.switchToStreetView();
			break;
		}
		mPerspective = perspective;

	}

	public int getPerspective() {
		return mPerspective;
	}

	@Override
	public void onWorldReady() {

		if (objectPosition != null) {
			renderer.addNewLandmark(objectPosition);
			renderer.lookAt(objectPosition);
		}

		getActivity().runOnUiThread(new Runnable() {
			@Override
			public void run() {
				tryLoading();
			}
		});
	}
}
